package org.kia.data;

import java.io.IOException;
import java.lang.reflect.Type;
import java.net.ConnectException;
import java.util.ArrayList;
import java.util.List;

import org.kia.commons.ClassUtil;
import org.kia.data.adapters.RequestAdapter;
import org.kia.data.adapters.ResponseAdapter;
import org.kia.data.interfaces.OnDataReceivedListener;

import android.content.Context;
import android.os.AsyncTask;
import android.util.Log;

import com.github.droidfu.cachefu.CachedModel;
import com.github.droidfu.cachefu.ModelCache;
import com.google.gson.Gson;

/**
 * Controller for all data related requests. Support for both local and remote
 * synchronized/asynchronous calls
 * 
 * @author Amir Lazarovich
 */
@SuppressWarnings(value = {"unchecked", "rawtypes"})
public class DataController {

	// ////////////////////////////////////////////
	// Constants
	// ////////////////////////////////////////////
	private static final String TAG = "DataController";

	/**
	 * The element size of the cache
	 */
	static final int DEFAULT_CAPACITY = 4;

	/**
	 * Cache expiration in minutes
	 */
	static final long DEFAULT_EXPIRATION_IN_MIN = 60;

	/**
	 * Maximum number of concurrent threads acting on the cache
	 */
	static final int DEFAULT_MAX_CONCURRENT_THREADS = 1;

	/**
	 * Default storage device
	 */
	static final int DEFAULT_STORAGE_DEVICE = ModelCache.DISK_CACHE_INTERNAL;

	// ////////////////////////////////////////////
	// Members
	// ////////////////////////////////////////////
	private OnDataReceivedListener mListener;
	private ModelCache mCache;

	// ////////////////////////////////////////////
	// Constructors & Initializers
	// ////////////////////////////////////////////

	/**
	 * Constructs a new {@link DataController} with default caching parameters: <br/>
	 * <li>
	 * {@link #DEFAULT_CAPACITY}</li> <li> {@link #DEFAULT_EXPIRATION_IN_MIN}</li>
	 * <li> {@link #DEFAULT_MAX_CONCURRENT_THREADS}</li> <li>
	 * {@link #DEFAULT_STORAGE_DEVICE}</li>
	 * 
	 * @param context
	 *            The context sent to the
	 *            {@link com.github.droidfu.cachefu.ModelCache}
	 * @param listener
	 *            The callback for all asynchronous responses
	 */
	public DataController(Context context, OnDataReceivedListener listener) {
		init(context, listener);
	}

	/**
	 * Initialize this data controller
	 * 
	 * @param context
	 * @param listener
	 */
	private void init(Context context, OnDataReceivedListener listener) {
		mCache = new ModelCache(DEFAULT_CAPACITY, DEFAULT_EXPIRATION_IN_MIN, DEFAULT_MAX_CONCURRENT_THREADS);
		// Enable disk cache
		mCache.enableDiskCache(context, DEFAULT_STORAGE_DEVICE);
		mListener = listener;
	}

	// ////////////////////////////////////////////
	// Public
	// ////////////////////////////////////////////

	/**
	 * Get data (local/remote) in asynchronous fashion
	 * 
	 * @param <T>
	 *            A subclass of {@link com.github.droidfu.cachefu.CachedModel} -
	 *            used for casting locally cached model and/or parsing remote
	 *            JSON strings
	 * @param targetClassType
	 *            The type of T
	 * @param key
	 *            A hash key - used for fetching the CachedModel
	 * @param request
	 *            a
	 *            {@link com.RequestAdapter.commons.net.adapters.VidRequestAdapter}
	 * @return A {@link android.os.AsyncTask} representing the asynchronous call
	 */
	public <T> AsyncTask<RequestAdapter, Integer, List<T>> getDataAsync(Type targetClassType,
			String key, RequestAdapter request) {
		AsyncTask<RequestAdapter, Integer, List<T>> task = new GetDataTask<T>(targetClassType);
		return task.execute(request);
	}

	/**
	 * Tries to find the data first in cache (locally) and if not found, tries
	 * from remote
	 * 
	 * @param <T>
	 *            A subclass of {@link com.github.droidfu.cachefu.CachedModel} -
	 *            used for casting locally cached model and/or parsing remote
	 *            JSON strings
	 * @param targetClassType
	 *            The {@link Type} of T
	 * @param key
	 *            A hash key - used for fetching the CachedModel
	 * @param request
	 *            a
	 *            {@link com.RequestAdapter.commons.net.adapters.VidRequestAdapter}
	 *            required for remote http request
	 * @return The model based on given targetClass or null in case nothing was
	 *         found
	 * 
	 * @throws DataFailureException
	 */
	public <T> T getData(Type targetClassType, String key, RequestAdapter request) {
		// Check local resources first
		T cachedModel = (T) getLocalData(targetClassType, key);
		if (cachedModel == null) {
			// Nothing found in local cache, requesting data from remote server
			// as well
			cachedModel = (T) getRemoteData(targetClassType, request);
		}

		return cachedModel;
	}

	/**
	 * Get local data
	 * 
	 * @param <T>
	 *            A subclass of {@link com.github.droidfu.cachefu.CachedModel} -
	 *            used for casting locally cached model
	 * @param targetClassType
	 *            The {@link Type} of T
	 * @param key
	 *            A hash key - used for fetching the CachedModel
	 * @return The cached model or null in case not found
	 */
	public <T extends CachedModel> T getLocalData(Type targetClassType, String key) {
		T cachedModel = ClassUtil.<T> getClass(targetClassType).cast(mCache.get(key));
		return cachedModel;
	}

	/**
	 * Get remote data
	 * 
	 * @param <T>
	 *            A subclass of {@link com.github.droidfu.cachefu.CachedModel} -
	 *            used for parsing/casting remote JSON string
	 * @param targetClassType
	 *            The .class type of T
	 * @param request
	 *            a
	 *            {@link com.RequestAdapter.commons.net.adapters.VidRequestAdapter}
	 * @return
	 * 
	 * @throws DataFailureException
	 */
	public <T> T getRemoteData(Type targetClassType, RequestAdapter request) {
		T cachedModel = null;
		Integer statusCode = null;
		try {
			ResponseAdapter response = request.send();
			statusCode = response.getStatusCode();
			String body = response.getResponseBodyAsString();

			// Parse...
			Gson gson = new Gson();
			cachedModel = gson.fromJson(body, targetClassType);
			
			// TODO handle local caching
//			cachedModel.setId(request.toString());
//			// Caching locally
//			cachedModel.save(mCache);
		} catch (ConnectException e) {
			Log.e(TAG, String.format("Couldn't connect to remote data source. Response status code: %d", statusCode), e);
		} catch (IOException e) {
			Log.e(TAG, String.format("Couldn't get response body as string. Response status code: %d", statusCode), e);
		} catch (Exception e) {
			Log.e(TAG, String.format("Couldn't get response json string. Response status code: %d", statusCode), e);
		}
		
		return cachedModel;
	}

	/**
	 * Post remote result to all listeners
	 * 
	 * @param results
	 */
	private <ResponseType> void postResultToListeners(
			List<ResponseType> results, List<RequestAdapter> requests) {
		if (mListener != null) {
			int counter = 0;
			for (ResponseType result : results) {
				if (result == null) {
					mListener.onDataFailure(requests.get(counter), 0, null);
				} else {
					mListener.onDataReceived(result);
				}
				counter++;
			}
		}
	}

	// ////////////////////////////////////////////
	// Inner classes
	// ////////////////////////////////////////////

	/**
	 * Task for asynchronous local & remote http requests
	 * 
	 * @param <T>
	 * @author Amir Lazarovich
	 */
	protected class GetDataTask<T> extends AsyncTask<RequestAdapter, Integer, List<T>> {

		private final Type mTargetClassType;
		private List<RequestAdapter> mRequests;

		/**
		 * Constructs a new data task
		 * 
		 * @param targetClassType
		 */
		GetDataTask(Type targetClassType) {
			mTargetClassType = targetClassType;
		}

		@Override
		protected List<T> doInBackground(RequestAdapter... requests) {
			List<T> responses = new ArrayList<T>(requests.length);
			mRequests = new ArrayList<RequestAdapter>(requests.length);
			RequestAdapter request;
			// Iterate over all pairs (key, request) passed
			for (int i = 0; i < requests.length; i++) {
				request = requests[i];
				mRequests.add(request);

				// Get data
				responses.add((T) getData(mTargetClassType, request.getKey(), request));

				// Publish progress
				publishProgress(i);
			}

			return responses;
		}

		@Override
		protected void onPreExecute() {
			super.onPreExecute();

		}

		@Override
		protected void onProgressUpdate(Integer... values) {
			super.onProgressUpdate(values);
		}

		@Override
		protected void onCancelled() {
			super.onCancelled();
		}

		@Override
		protected void onPostExecute(List<T> result) {
			super.onPostExecute(result);
			postResultToListeners(result, mRequests);
		}
	}

}
